<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Lucas-Kanade Optical Flow Example</title>
<link href="js_example_style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h2>Lucas-Kanade Optical Flow Example</h2>
<p>
    Click <b>Start/Stop</b> button to start or stop the video.<br>
    The <b>videoInput</b> is a &lt;video&gt; element used as input.
    The <b>canvasOutput</b> is a &lt;canvas&gt; element used as output.<br>
    To decide the points, we use <b>cv.goodFeaturesToTrack()</b>. We take the first frame, detect some Shi-Tomasi corner points in it, then we iteratively track those points using <b>cv.calcOpticalFlowPyrLK</b>.<br>
    The code of &lt;textarea&gt; will be executed when video is started.<br>
    You can modify the code to investigate more.
</p>
<div>
<div class="control"><button id="startAndStop" disabled>Start</button></div>
<textarea class="code" rows="29" cols="100" id="codeEditor" spellcheck="false">
</textarea>
</div>
<p class="err" id="errorMessage"></p>
<div>
    <table cellpadding="0" cellspacing="0" width="0" border="0">
    <tr>
        <td>
            <video id="videoInput" width="320" height="240" muted></video>
        </td>
        <td>
            <canvas id="canvasOutput" width="320" height="240" ></canvas>
        </td>
        <td></td>
        <td></td>
    </tr>
    <tr>
        <td>
            <div class="caption">videoInput</div>
        </td>
        <td>
            <div class="caption">canvasOutput</div>
        </td>
        <td></td>
        <td></td>
    </tr>
    </table>
</div>
<script src="https://webrtc.github.io/adapter/adapter-5.0.4.js" type="text/javascript"></script>
<script src="utils.js" type="text/javascript"></script>
<script id="codeSnippet" type="text/code-snippet">
let video = document.getElementById('videoInput');
let cap = new cv.VideoCapture(video);

// parameters for ShiTomasi corner detection
let [maxCorners, qualityLevel, minDistance, blockSize] = [30, 0.3, 7, 7];

// parameters for lucas kanade optical flow
let winSize = new cv.Size(15, 15);
let maxLevel = 2;
let criteria = new cv.TermCriteria(cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03);

// create some random colors
let color = [];
for (let i = 0; i < maxCorners; i++) {
    color.push(new cv.Scalar(parseInt(Math.random()*255), parseInt(Math.random()*255),
                             parseInt(Math.random()*255), 255));
}

// take first frame and find corners in it
let oldFrame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
cap.read(oldFrame);
let oldGray = new cv.Mat();
cv.cvtColor(oldFrame, oldGray, cv.COLOR_RGB2GRAY);
let p0 = new cv.Mat();
let none = new cv.Mat();
cv.goodFeaturesToTrack(oldGray, p0, maxCorners, qualityLevel, minDistance, none, blockSize);

// Create a mask image for drawing purposes
let zeroEle = new cv.Scalar(0, 0, 0, 255);
let mask = new cv.Mat(oldFrame.rows, oldFrame.cols, oldFrame.type(), zeroEle);

let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let frameGray = new cv.Mat();
let p1 = new cv.Mat();
let st = new cv.Mat();
let err = new cv.Mat();

const FPS = 30;
function processVideo() {
    try {
        if (!streaming) {
            // clean and stop.
            frame.delete(); oldGray.delete(); p0.delete(); p1.delete(); err.delete(); mask.delete();
            return;
        }
        let begin = Date.now();

        // start processing.
        cap.read(frame);
        cv.cvtColor(frame, frameGray, cv.COLOR_RGBA2GRAY);

        // calculate optical flow
        cv.calcOpticalFlowPyrLK(oldGray, frameGray, p0, p1, st, err, winSize, maxLevel, criteria);

        // select good points
        let goodNew = [];
        let goodOld = [];
        for (let i = 0; i < st.rows; i++) {
            if (st.data[i] === 1) {
                goodNew.push(new cv.Point(p1.data32F[i*2], p1.data32F[i*2+1]));
                goodOld.push(new cv.Point(p0.data32F[i*2], p0.data32F[i*2+1]));
            }
        }

        // draw the tracks
        for (let i = 0; i < goodNew.length; i++) {
            cv.line(mask, goodNew[i], goodOld[i], color[i], 2);
            cv.circle(frame, goodNew[i], 5, color[i], -1);
        }
        cv.add(frame, mask, frame);

        cv.imshow('canvasOutput', frame);

        // now update the previous frame and previous points
        frameGray.copyTo(oldGray);
        p0.delete(); p0 = null;
        p0 = new cv.Mat(goodNew.length, 1, cv.CV_32FC2);
        for (let i = 0; i < goodNew.length; i++) {
            p0.data32F[i*2] = goodNew[i].x;
            p0.data32F[i*2+1] = goodNew[i].y;
        }

        // schedule the next one.
        let delay = 1000/FPS - (Date.now() - begin);
        setTimeout(processVideo, delay);
    } catch (err) {
        utils.printError(err);
    }
};

// schedule the first one.
setTimeout(processVideo, 0);
</script>
<script type="text/javascript">
let utils = new Utils('errorMessage');

utils.loadCode('codeSnippet', 'codeEditor');

let streaming = false;
let videoInput = document.getElementById('videoInput');
let startAndStop = document.getElementById('startAndStop');

startAndStop.addEventListener('click', () => {
    if (!streaming) {
        utils.clearError();
        videoInput.play().then(() => {
            onVideoStarted();
        });
    } else {
        videoInput.pause();
        videoInput.currentTime = 0;
        onVideoStopped();
    }
});

function onVideoStarted() {
    streaming = true;
    startAndStop.innerText = 'Stop';
    videoInput.height = videoInput.width * (videoInput.videoHeight / videoInput.videoWidth);
    utils.executeCode('codeEditor');
}

function onVideoStopped() {
    streaming = false;
    startAndStop.innerText = 'Start';
}

videoInput.addEventListener('ended', () => {
    onVideoStopped();
});

utils.loadOpenCv(() => {
    videoInput.addEventListener('canplay', () => {
        startAndStop.removeAttribute('disabled');
    });
    videoInput.src = 'box.mp4';
});
</script>
</body>
</html>
